logo头像
Snippet 博客主题

对javascript EventLoop事件循环机制不一样的理解

本文于962天之前发表,文中内容可能已经过时。

前置知识点:

事件循环机制

相信读者读完以上推荐的文章后,已经知道事件循环机制是怎么一回事了吧,也能从容应对面试。接下来我要谈谈自己的理解:

为什么会有事件循环机制

  • js设计之初就是单线程模式,代码也都是顺序执行,当遇到因为大量计算、http请求等需要额外的等待时间时,浏览器用户就会体验到卡顿了,所以所有的设计和改进初衷只有一个就是要快

事件循环机制的产生

  • 浏览器说我的内核是多线程,可以辅助JS引擎线程啊,Web Worker线程提供大量计算辅助(不能操作DOM),事件触发线程定时触发器线程异步http请求线程

  • 执行栈(先进后出),由JS引擎线程控制,引用下面这个例子谈谈自己的理解:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    console.log('script start');

    setTimeout(function() {
    console.log('setTimeout');
    }, 0);

    Promise.resolve().then(function() {
    console.log('promise1');
    }).then(function() {
    console.log('promise2');
    });
    console.log('script end');

    // "script start"
    // "script end"
    // "promise1"
    // "promise2"
    // "setTimeout"
  • ES5还没有Promise时代,异步回调很常见,上面例子中,通过解读Promise源码(前端面试必考题Promise的源码解析),我们可以把Promise转换成如下图式回调(个人理解,文章中的Promise源码也只是模拟,大部分浏览器已经原生支持)。

    1. 打印完script start, script end主执行栈出栈,如果Promise.resolve().then换成new Promise(executor),脑补Promise换成回调函数,那么这个函数一执行,executor函数也就执行了,然后遇到异步回调,回调函数被其它对应的线程接手,启动观察者模式,完成后回调函数被推入事件任务队列,等待执行栈空了进入主线程执行

    2. 以上这种在异步函数中放同步函数的例子,为了合理解释输出顺序而推出了microtasks微任务的概念,请看下面的例子,脑补Promise换成回调函数,Promise.prototype.then内部执行了return new Promise(),js引擎在捕捉到Promise时,放到了由js引擎自身控制的微任务队列等待执行,也就造成promise1、2、3、4错开打印

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      console.log('script start');

      new Promise(function(resolve, reject) {
      console.log('promise1');
      resolve();
      }).then(function() {
      console.log('promise2');
      });

      new Promise(function(resolve, reject) {
      console.log('promise3');
      resolve();
      }).then(function() {
      console.log('promise4');
      });

      console.log('script end');

      // "script start"
      // "promise1"
      // "promise3"
      // "script end"
      // "promise2"
      // "promise4"
    3. microtasks微任务的概念完全为了解释异步函数中放同步函数的场景,而且各类文章和面试都是这种题目和例子,在实际开发过程中,你会在Promise中这么写么?,在我看来这种比较打印顺序太过于理论,而且可能会混乱你的思绪。就像下面的例子,Promiseresolve决定了Promise状态,就像在回调函数中满足了条件才会继续执行,例子中只是用setTimeout模拟异步请求,用之前的理论你可能觉得setTimeout被放入了事件任务队列,那没有resolvePromise怎么解释呢?(放到微任务里一直阻碍第一个setTimeout宏任务执行吗?显然是不可能的,这不是跟设计事件循环机制初衷冲突了么

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      setTimeout(function() {
      console.log('setTimeout');
      }, 0);

      new Promise(function(resolve, reject) {
      setTimeout(function() { // 模拟异步请求
      console.log('promise1');
      resolve();
      }, 0);
      }).then(function() {
      console.log('promise2');
      });

      new Promise(function(resolve, reject) {
      // resolve(); 注释掉resolve,使Promise一直处于‘pending’状态
      }).then(function() {
      console.log('promise2');
      });

      // "setTimeout"
      // "promise1"
      // "promise2"
    4. 个人认为把Promiseasync/await脑补成原始的回调函数(模拟源码中模拟异步是用的setTimeout函数),而js引擎捕捉到setTimeout, setInterval就转给定时触发器线程处理,捕捉到XMLHttpReuqest, fetch就转给异步http请求线程,跟事件触发线程一起管理着事件任务队列,微任务的概念可以看作是当事件触发线程遇到几乎同时需要把回调函数放到事件任务队列时,Promise内部的异步标识函数优先级高于setTimeout函数吧,以上例子中没有执行resolvePromise状态一直处于’pending’,事件触发线程压根没有放入到事件任务队列,总之浏览器会安排的妥妥的,不要打架,虽然js引擎线程只有一个(听我指挥排好队,咱们这都是同步代码执行ms级别,我开了很多其它线程处理需要等待的代码了)。以下例子模拟所谓的几乎同时把回调函数放到事件任务队列,记得把Promise脑补成原始的回调函数。仿佛回到了没有微任务的时代。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      setTimeout(function() {
      console.log('setTimeout');
      }, 0);

      new Promise(function(resolve, reject) {
      setTimeout(function() { // 模拟异步请求
      console.log('promise1');
      resolve();
      }, 2000);
      }).then(function() {
      console.log('promise2');
      });

      new Promise(function(resolve, reject) {
      setTimeout(function() { // 模拟异步请求
      console.log('promise3');
      resolve();
      }, 2000);
      }).then(function() {
      console.log('promise4');
      });

      // "setTimeout"
      // "promise1"
      // "promise2"
      // "promise3"
      // "promise4"

以上内容纯属未深入了解js情况下的个人理解,感觉是在努力摒弃微任务的概念,回归ES5回调函数时代,便于自身理解事件循环机制而做出的遐想。

  1. 2022-03-12更新对第4点中误解,多个Promise接上多个then时(前提是多个Promise处于同一任务中,假如分别在两个setTimeout中并且都是在同一时刻触发的,是会先执行完一条链路的Promise),只有每执行一次后才知道当前Promise的状态,假如是rejected那么将忽略后续的then,所以后续的then并没有进入微任务队列。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    Promise.resolve().then(() => {
    console.log(0);
    return new Promise((resolve, reject) => {
    resolve(4);
    })
    /**
    以上暂且理解为两个then,与后续的then交替执行
    正解参考:https://github.com/rhinel/blog-word/issues/4
    */
    })
    .then((res) => {
    console.log(res);
    })

    Promise.resolve()
    .then(() => {
    console.log(1);
    })
    .then(() => {
    console.log(2);
    })
    .then(() => {
    console.log(3);
    })
    .then(() => {
    console.log(5);
    })
    .then(() =>{
    console.log(6);
    })

    // 0 1 2 3 4 5 6
  2. js宏任务、微任务面试题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    async function async1(){
    console.log('1')
    await async2()
    console.log('2')
    }
    async function async2(){
    console.log('3')
    }
    console.log('4')
    setTimeout(function(){
    console.log('5')
    },0)
    async1();
    new Promise(function(resolve){
    console.log('6')
    resolve();
    }).then(function(){
    console.log('7')
    })
    console.log('8')

    // 4,1,3,6,8,2,7,5
  3. MacroTask and MicroTask execution order

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    for (let i = 0; i < 2; i++) {
    setTimeout(() => {
    console.log("Timeout ", i);
    Promise.resolve().then(() => {
    console.log("Promise 1 ", i);
    }).then(() => {
    console.log("Promise 2 ", i);
    });
    Promise.resolve().then(() => {
    console.log("Promise 3 ", i);
    }).then(() => {
    console.log("Promise 4 ", i);
    });
    })
    }

    // Timeout 0
    // Promise 1 0
    // Promise 3 0
    // Promise 2 0
    // Promise 4 0
    // Timeout 1
    // Promise 1 1
    // Promise 3 1
    // Promise 2 1
    // Promise 4 1

Node.js环境下的事件循环机制(6个阶段)

  • Node.js如何理解process.nextTick, 其机制在于执行完当前阶段同步代码后,在进入下一个阶段前执行传入的回调函数,跟setImmediate相比更像是立即执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
setTimeout(() => {
console.log(1);
})
setTimeout(() => {
console.log(2);
})
setTimeout(() => {
process.nextTick(console.log, 3);
})
setTimeout(() => {
console.log(4);
})
setTimeout(() => {
console.log(5);
})
process.nextTick(console.log, 0);

/**
node11之前:0 1 2 4 5 3
node11之后:0 1 2 3 4 5
0 还是会最先执行的,类比同一执行栈中 Promise.then 先于 setTimeout
*/
  • setTimeoutsetImmediate 的先后运行顺序

参考Node.js系统架构图,Event Loop 是在由C语言编写的libuv中运行,而js代码是运行在由C++编写的V8引擎上。

  1. 以下示例代码输出的先后顺序是随机的,由于libuvv8属于独立的主模块,启动的先后顺序取决于当前各自进程的性能。

    1
    2
    3
    4
    5
    6
    7
    setTimeout(() => {
    console.log('setTimeout');
    })

    setImmediate(() => {
    console.log('setImmediate');
    })
  2. 把上面代码放到 I/O 操作的回调里,setImmediate 的回调就总是优先于 setTimeout 的回调,很好理解此时按照正常的Event Loop阶段顺序,I/O属于pollsetImmediate属于checksetTimeout属于timers

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const fs = require('fs');

    fs.readFile(__filename, () => {
    setTimeout(() => {
    console.log('timeout');
    }, 0);
    setImmediate(() => {
    console.log('immediate');
    });
    });

推荐阅读:

参考文章:

微信打赏

赞赏是不耍流氓的鼓励